Repository navigation
Add native VNC panel with input routing and tests - #2878
lawrencecchen wants to merge 18 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds first-class VNC support across model, UI, persistence, CLI, and tests: new VncPanel implementation (native + web renderers), WebSocket proxy bridge, discovery/persistence, workspace/tab integration, localization, test stability changes, and end-to-end UI tests for input routing. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User
participant UI as VncPanelView
participant Panel as VncPanel
participant Proxy as VncWebSocketProxyBridge
participant Renderer as Native/Web Renderer
participant Server as Remote VNC Server
User->>UI: enter endpoint / credentials
UI->>Panel: set inputs / connect()
Panel->>Proxy: start(loopbackListener -> connect remoteHost:port)
Proxy->>Server: TCP connect / handshake
Server->>Proxy: send framebuffer / accept input
Proxy->>Renderer: forward frames
User->>Renderer: keyboard / mouse / IME events
Renderer->>Proxy: forward input frames
Proxy->>Server: forward input to remote
Panel->>Panel: update connectionState (connecting → connected / error)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR adds a native VNC panel backed by macOS's private Two issues need attention before merging:
Confidence Score: 3/5Not safe to merge as-is due to a main-thread block on connect and a credential-exposure risk via external CDN. Two P1 findings: synchronous process.waitUntilExit on the main actor will freeze the UI on SSH alias lookup, and loading noVNC from an external CDN without SRI passes plaintext VNC passwords through externally-controlled code. Both are on the hot path for every VNC connection. Sources/Panels/MarkdownPanel.swift (VncSSHHostAliasResolver.resolve call in connect(), and the viewerHTML CDN import)
|
| Filename | Overview |
|---|---|
| Sources/Panels/MarkdownPanel.swift | Adds entire VNC panel implementation (~1800 lines) appended to MarkdownPanel.swift; two P1 issues: main-thread blocking in VncSSHHostAliasResolver and external CDN noVNC load without SRI |
| Sources/Panels/MarkdownPanelView.swift | Appends VncPanelView and related representables to MarkdownPanelView.swift; logic is correct but file placement is architecturally inconsistent |
| Sources/SessionPersistence.swift | Adds SessionVncPanelSnapshot with non-optional autoConnect Bool lacking a default value, which will fail Codable decoding if the key is absent in future schema changes |
| Sources/Workspace.swift | Correctly wires newVncSurface/newVncSplit, session snapshot/restore, config surface apply, and BonsplitDelegate tab-kind routing for the VNC panel type |
| Sources/TabManager.swift | Adds focusedVncPanel, vncPanel(tabId:panelId:), and openVnc() helpers following the same pattern as existing browser equivalents |
| Sources/TerminalController.swift | Adds vnc_state and surface.vnc_state socket commands plus VNC surface/pane creation; uses DispatchQueue.main.sync appropriately for one-shot snapshot queries |
| Sources/AppDelegate.swift | Test harness improvements: app activation before UI-test setup, inline base64 HTML page replacing the https://example.com dependency, and re-applied shortcut overrides for deterministic CI routing |
| cmuxUITests/VncPanelInputRoutingUITests.swift | New UI tests exercise keyboard (multi-key, modified), mouse, drag, and IME/Unicode input through the fake-native VNC session; socket-based polling pattern matches existing test style |
| Sources/ContentView.swift | Adds VNC to command palette kind/keywords and registers palette.newVncTab; routes focus for VncPanel.webView in the panel-focus switch |
| Resources/Localizable.xcstrings | All new VNC UI strings properly localized in both English and Japanese following project conventions |
Sequence Diagram
sequenceDiagram
participant UI as VncPanelView (SwiftUI)
participant VP as VncPanel (@MainActor)
participant NSC as VncNativeSessionController
participant SSH as VncSSHHostAliasResolver
participant SS as ScreenSharing.framework
participant WB as VncWebSocketProxyBridge
participant WV as WKWebView (noVNC)
participant CDN as novnc.com CDN
UI->>VP: connect()
VP->>SSH: resolve(host:) [BLOCKS main thread]
SSH-->>VP: resolvedHost
alt Native renderer available
VP->>NSC: connect(targetHost:port:username:password:)
NSC->>SS: connectToAddress:withOptions:
SS-->>NSC: state updates (polling)
NSC-->>VP: onStateChange(.connected)
else WebView fallback
VP->>WB: start(remoteHost:remotePort:)
WB-->>VP: .listenerReady(port)
VP->>WV: evaluateJavaScript cmuxVncConnect(wsURL, user, pass)
WV->>CDN: import RFB from novnc.com [no SRI]
CDN-->>WV: rfb.js (external code receives credentials)
WV-->>VP: postMessage state updates
end
Reviews (1): Last reviewed commit: "fix: restore bonsplit pointer after main..." | Re-trigger Greptile
| if let nativeSessionController { | ||
| let resolvedHost = VncSSHHostAliasResolver.resolve(host: target.host) ?? target.host | ||
| nativeSessionController.connect( | ||
| targetHost: resolvedHost, | ||
| port: target.port, | ||
| username: usernameInput, | ||
| password: passwordInput | ||
| ) | ||
| return | ||
| } | ||
|
|
||
| let bridge = VncWebSocketProxyBridge() | ||
| proxyBridge = bridge | ||
| let resolvedHost = VncSSHHostAliasResolver.resolve(host: target.host) ?? target.host |
There was a problem hiding this comment.
VncSSHHostAliasResolver.resolve() blocks the main thread
connect() is @MainActor, but VncSSHHostAliasResolver.resolve() calls process.waitUntilExit() synchronously. Any time a user connects to a VNC target that triggers SSH config lookup, the UI event loop freezes until /usr/bin/ssh -G <host> returns. While ssh -G is usually fast, slow NFS-mounted SSH config directories or unresponsive ControlMaster sockets can block for several seconds.
The resolver should run on a background queue and complete asynchronously:
// In connect()
let capturedHost = target.host
Task.detached(priority: .userInitiated) {
let resolvedHost = VncSSHHostAliasResolver.resolve(host: capturedHost) ?? capturedHost
await MainActor.run { [weak self] in
self?.finishConnect(resolvedHost: resolvedHost, target: target)
}
}There was a problem hiding this comment.
Moved SSH alias resolution off the main actor and gated the async completion with a connect-attempt token before starting the native session.
— Claude Code
| <body> | ||
| <div id="screen" tabindex="0"></div> | ||
| <script type="module" crossorigin="anonymous"> | ||
| import RFB from "https://novnc.com/noVNC/core/rfb.js"; |
There was a problem hiding this comment.
noVNC loaded from external CDN without subresource integrity
The webview fallback path dynamically fetches rfb.js from https://novnc.com at connection time with no SRI hash. Every window.cmuxVncConnect(wsURL, username, password) and window.cmuxVncSendCredentials(username, password) call passes plaintext credentials to this CDN-sourced code. A compromised or MITM'd CDN response could silently exfiltrate VNC credentials.
This also means the webview renderer path is completely broken in offline/air-gapped environments.
Consider bundling the noVNC library as a local asset and loading it from the app bundle, or at minimum adding a Subresource Integrity hash if the CDN URL must be kept.
There was a problem hiding this comment.
Removed the runtime noVNC CDN import from the fallback viewer HTML, so that path no longer pulls credential-handling code from novnc.com at connect time.
— Claude Code
| struct VncPanelTarget: Equatable, Sendable { | ||
| static let defaultPort = 5900 |
There was a problem hiding this comment.
VNC implementation placed in
MarkdownPanel.swift
The entire VNC feature (~1 800 lines spanning VncPanelTarget, VncBonjourDiscovery, VncWebSocketProxyBridge, VncNativeSessionController, and VncPanel) is appended to MarkdownPanel.swift. These two features share no code and the file already had 185 lines before this PR. Consider moving the VNC implementation to a dedicated VncPanel.swift (and the view to VncPanelView.swift) to keep each file cohesive and to make future diffs easier to read.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| } | ||
|
|
||
| struct SessionVncPanelSnapshot: Codable, Sendable { | ||
| var endpointInput: String? | ||
| var usernameInput: String? | ||
| var autoConnect: Bool |
There was a problem hiding this comment.
autoConnect: Bool has no Codable default value
autoConnect is non-optional with no default. If a future schema revision omits the field (or an old client encounters a payload written by a newer client that dropped the key), JSONDecoder will throw and the entire session panel will fail to restore. Adding var autoConnect: Bool = false makes the struct tolerant of that case.
| } | |
| struct SessionVncPanelSnapshot: Codable, Sendable { | |
| var endpointInput: String? | |
| var usernameInput: String? | |
| var autoConnect: Bool | |
| struct SessionVncPanelSnapshot: Codable, Sendable { | |
| var endpointInput: String? | |
| var usernameInput: String? | |
| var autoConnect: Bool = false | |
| } |
There was a problem hiding this comment.
Added a default of false for SessionVncPanelSnapshot.autoConnect so older or partial payloads can still decode cleanly.
— Claude Code
| private static func javaScriptStringLiteral(_ value: String) -> String { | ||
| let escaped = value | ||
| .replacingOccurrences(of: "\\", with: "\\\\") | ||
| .replacingOccurrences(of: "\"", with: "\\\"") | ||
| .replacingOccurrences(of: "\n", with: "\\n") | ||
| .replacingOccurrences(of: "\r", with: "\\r") | ||
| return "\"\(escaped)\"" | ||
| } |
There was a problem hiding this comment.
javaScriptStringLiteral doesn't escape null bytes
The function escapes \, ", \n, and \r but leaves null bytes (\0) unescaped. A VNC password containing a null byte (unusual but not impossible) would silently truncate the JavaScript string at that point in some engines, resulting in an incorrect credential being sent.
There was a problem hiding this comment.
Escaped null bytes in javaScriptStringLiteral so credential payloads no longer truncate on embedded NUL characters.
— Claude Code
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5d508dc077
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| .onChange(of: panel.connectionState) { state in | ||
| if state == .connected { | ||
| panel.focus() |
There was a problem hiding this comment.
Gate VNC auto-focus to the currently focused pane
The connection-state observer always calls panel.focus() when the VNC session reaches connected, even when this panel is not the active pane (isFocused == false). In auto-connect flows (for example, session restore or non-focused split creation), this can steal first responder from the user’s current panel and redirect subsequent keystrokes unexpectedly. Only focusing when isFocused is true avoids this focus-jump regression.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Gated the connected-state autofocus on isFocused so background auto-connects no longer steal first responder from the active pane.
— Claude Code
| return panel.connectionState == .connecting || | ||
| panel.endpointInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty |
There was a problem hiding this comment.
Allow canceling an in-progress VNC connection from the toolbar
The connect button is disabled whenever connectionState == .connecting, but connectOrDisconnect() explicitly supports disconnecting in that state. This makes the cancel path unreachable from the button and leaves users unable to stop a hanging connection attempt through normal UI interaction. Keep the button enabled during connecting (or remove the dead disconnect branch) so in-flight connects can be canceled.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Kept the toolbar button enabled during connecting so connectOrDisconnect() can cancel an in-flight connection attempt.
— Claude Code
| throw NSError( | ||
| domain: "cmux.vnc.proxy", | ||
| code: 1, | ||
| userInfo: [NSLocalizedDescriptionKey: "Invalid remote VNC port."] |
There was a problem hiding this comment.
Localize VNC proxy error messages before surfacing them
This adds hardcoded English error text (NSLocalizedDescriptionKey) that is propagated to lastErrorDetail and rendered directly in the VNC toolbar error area, so Japanese users receive untranslated UI strings. The repo rule in /workspace/cmux/AGENTS.md requires all user-facing strings to be localized via String(localized:...); these emitted messages should use localized keys instead of literal English.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Localized the remaining VNC proxy and viewer-runtime strings before surfacing them through the toolbar error state.
— Claude Code
There was a problem hiding this comment.
Actionable comments posted: 16
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
Sources/AppDelegate.swift (1)
8563-8592:⚠️ Potential issue | 🟠 MajorResolve the UITest window from the same
TabManageryou're about to mutate.
mainTerminalWindow()can still front a different main window than thetabManagercaptured below because it just picksNSApp.windows.first. If the XCTest fallback window is still alive, this reintroduces nondeterministic key routing by activating one window and creating the split in another.Based on learnings: during XCTest launch stabilization, the app may force-create a fallback main window when `NSApp.windows` is empty, and that fallback is only closed once the real window registers.Suggested fix
@@ - func mainTerminalWindow() -> NSWindow? { - NSApp.windows.first { window in - guard let raw = window.identifier?.rawValue else { return false } - return raw == "cmux.main" || raw.hasPrefix("cmux.main.") - } - } + func mainTerminalWindow(for tabManager: TabManager? = self.tabManager) -> NSWindow? { + if let tabManager, + let windowId = self.windowId(for: tabManager), + let window = self.mainWindow(for: windowId) { + return window + } + if let keyWindow = NSApp.keyWindow, self.isMainTerminalWindow(keyWindow) { + return keyWindow + } + if let mainWindow = NSApp.mainWindow, self.isMainTerminalWindow(mainWindow) { + return mainWindow + } + return NSApp.windows.first(where: { self.isMainTerminalWindow($0) }) + } @@ - if let window = mainTerminalWindow() { + if let window = mainTerminalWindow(for: tabManager) { + self.setActiveMainWindow(window) window.makeKeyAndOrderFront(nil) window.orderFrontRegardless() }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/AppDelegate.swift` around lines 8563 - 8592, The test can activate a different "fallback" window because mainTerminalWindow() searches NSApp.windows; change runSetupWhenWindowReady() to resolve the UI window from the same TabManager you're about to mutate: use a TabManager-backed window reference (e.g., a property or method on TabManager such as tabManager.mainWindow or tabManager.windowForUITests) instead of calling mainTerminalWindow(), and when making the window key/ordering or calling orderFrontRegardless(), operate on that TabManager-derived window; you can also update mainTerminalWindow() to accept a TabManager parameter or add a new helper that prefers tabManager's window to avoid the XCTest fallback race.cmuxUITests/MenuKeyEquivalentRoutingUITests.swift (1)
325-344:⚠️ Potential issue | 🟠 MajorKeep
refocusWebViewas a checked precondition.These shortcut tests only cover the WKWebView routing path when the web view actually regains first responder. Swallowing both waits lets them pass while the omnibar or another responder is still handling Command-key input.
🧪 Return the refocus result and assert it at the call sites
- private func refocusWebView(app: XCUIApplication) { + private func refocusWebView(app: XCUIApplication) -> Bool { app.activate() // Cmd+L focuses the omnibar (so WebKit is no longer first responder). app.typeKey("l", modifierFlags: [.command]) - _ = waitForGotoSplitMatch(timeout: 2.5) { data in + guard waitForGotoSplitMatch(timeout: 2.5) { data in data["webViewFocusedAfterAddressBarFocus"] == "false" - } + } else { + return false + } // Escape should leave the omnibar and focus WebKit again. // Send Escape twice: the first may only clear suggestions/editing state // (Chrome-like two-stage escape), the second triggers blur to WebView. app.typeKey(XCUIKeyboardKey.escape.rawValue, modifierFlags: []) if !waitForGotoSplitMatch(timeout: 2.0, predicate: { $0["webViewFocusedAfterAddressBarExit"] == "true" }) { app.typeKey(XCUIKeyboardKey.escape.rawValue, modifierFlags: []) } - _ = waitForGotoSplitMatch(timeout: 2.5) { data in + return waitForGotoSplitMatch(timeout: 2.5) { data in data["webViewFocusedAfterAddressBarExit"] == "true" } }Based on learnings,
performKeyEquivalent(with:) handles Command-key routing end-to-end ... in NSWindow.cmux_performKeyEquivalent ... after WKWebView focus, so these tests need to prove WKWebView regained first responder.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cmuxUITests/MenuKeyEquivalentRoutingUITests.swift` around lines 325 - 344, The helper refocusWebView currently swallows the final focus checks; change its signature to return a Bool indicating whether the WKWebView regained first responder and update its internals to return the result of the final wait predicate (use waitForGotoSplitMatch with the predicate data["webViewFocusedAfterAddressBarExit"] == "true" rather than discarding it), and ensure the intermediate escape retry still occurs but its outcome feeds into the returned Bool; then update every call site to assert the returned Bool (fail the test if false) so tests require the web view to actually regain focus.Sources/TerminalController.swift (1)
14416-14445:⚠️ Potential issue | 🟠 MajorSpace-splitting breaks quoted VNC credentials.
Lines 14416 and 15977 tokenize with
args.split(separator: " "), so--password="two words"or usernames with spaces get truncated before they reachnewVncSplit/newVncSurface. Since this feature introduces free-form credential flags, these command paths should go through the same option parser used byvncState(_:)instead of manual space splitting.Also applies to: 15977-16001
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/TerminalController.swift` around lines 14416 - 14445, The manual tokenization using args.split(separator: " ") in the TerminalController parsing block breaks quoted credentials (e.g. --password="two words"); replace the space-splitting logic and per-part handling with the same option parser used by vncState(_:) so flags with spaces/quotes are parsed correctly. Locate the parsing block that iterates over parts (the code using args.split and symbols like parseSplitDirection, panelType, direction, url, endpoint, username, password, autoConnect) and route the raw args string into the shared option-parsing routine (the one vncState(_) uses) or reuse the parsing helper used by newVncSplit/newVncSurface, ensuring quoted values are preserved and then map the resulting parsed options to panelType/direction/url/endpoint/username/password/autoConnect. Ensure both occurrences (this block and the similar block around newVncSplit/newVncSurface) are updated to use the same parser.
🧹 Nitpick comments (4)
Sources/AppDelegate.swift (1)
8594-8612: Consider extracting the goto-split shortcut seeding into one helper.These four
setShortcutcalls now live in two places in this setup path. A small helper would keep the initial seed and the later re-apply step from drifting.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/AppDelegate.swift` around lines 8594 - 8612, Extract the repeated four KeyboardShortcutSettings.setShortcut calls (the StoredShortcut(...) entries for .focusLeft, .focusRight, .focusUp, .focusDown) into a single helper function (e.g., seedGotoSplitShortcuts or applyGotoSplitShortcuts) and call that helper both here (inside the if !useGhosttyConfig block) and at the other setup location where they are currently duplicated; the helper should encapsulate creation of the StoredShortcut instances and the four setShortcut calls so future changes stay in one place.Sources/Workspace.swift (1)
451-452: Please add a VNC session round-trip test.This adds a dedicated VNC snapshot + rehydrate path, but the PR’s listed UI tests only exercise input routing. A small
sessionSnapshot()/restoreSessionSnapshot()round trip would cover endpoint, username, custom title, and auto-connect restore.Also applies to: 492-497, 519-520, 684-693, 758-760
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Workspace.swift` around lines 451 - 452, Add a unit test that exercises the VNC session snapshot and rehydrate path by creating a VNC session, calling sessionSnapshot(), then calling restoreSessionSnapshot() and asserting the restored SessionVncPanelSnapshot fields (endpoint, username, customTitle, autoConnect) match the original; update test coverage to include the vncSnapshot property alongside markdownSnapshot and ensure the restore path uses the SessionVncPanelSnapshot deserialization (check functions/session paths that reference vncSnapshot and restoreSessionSnapshot); repeat the same round-trip test pattern for other similar spots referenced (around the blocks at the other occurrences) so endpoint/username/title/auto-connect are validated end-to-end.cmuxUITests/BrowserPaneNavigationKeybindUITests.swift (2)
1379-1390: Don’t burn the full timeout before callingactivate().When launch lands in
.runningBackground, the firstwait(for: .runningForeground, timeout: timeout)can only fail after the full 12s expires, and only then do we try to recover. Because almost every test in this suite uses this helper, that background-launch path turns into minutes of avoidable delay on the exact CI instability this is trying to smooth out.Suggested fast-path
private func ensureForegroundAfterLaunch(_ app: XCUIApplication, timeout: TimeInterval) -> Bool { - if app.wait(for: .runningForeground, timeout: timeout) { - return true - } - if app.state == .runningBackground { - app.activate() - if app.wait(for: .runningForeground, timeout: 6.0) { - return true - } - } - app.activate() - return app.wait(for: .runningForeground, timeout: 2.0) + let initialProbeDeadline = Date().addingTimeInterval(min(timeout, 1.0)) + while Date() < initialProbeDeadline { + if app.state == .runningForeground { + return true + } + if app.state == .runningBackground { + app.activate() + return app.wait(for: .runningForeground, timeout: timeout) + } + RunLoop.current.run(until: Date().addingTimeInterval(0.1)) + } + + app.activate() + return app.wait(for: .runningForeground, timeout: max(timeout - 1.0, 2.0)) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cmuxUITests/BrowserPaneNavigationKeybindUITests.swift` around lines 1379 - 1390, The helper ensureForegroundAfterLaunch currently waits the full timeout before trying to recover from a .runningBackground launch; change its logic so you check app.state early and do not burn the full timeout: first, if app.state == .runningBackground then call app.activate() and immediately wait a short recovery timeout (e.g., 1–2s) for .runningForeground; otherwise try a short initial wait for .runningForeground and only if that fails and state is .runningBackground call activate() and wait a slightly longer recovery window; update the function ensureForegroundAfterLaunch to use these shorter/fallback waits to avoid waiting the entire timeout before calling activate().
1267-1280: Keep the fallback panel-scoped.If
BrowserPanelContent.\(panelId)is missing, this helper falls back toBrowserWebViewSurface.firstMatch, which ignores the requestedpanelId. That makes the click nondeterministic as soon as a scenario has more than one browser surface, and it can also hide a regression in the panel-specific accessibility identifier.Suggested tightening
private func clickBrowserPane(_ app: XCUIApplication, panelId: String, timeout: TimeInterval = 6.0) { let browserPane = app.otherElements["BrowserPanelContent.\(panelId)"].firstMatch if browserPane.waitForExistence(timeout: timeout) { browserPane.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).click() RunLoop.current.run(until: Date().addingTimeInterval(0.15)) return } // Some CI runs only expose the shared webview surface accessibility ID. - let browserSurface = app.otherElements["BrowserWebViewSurface"].firstMatch + let browserSurfaces = app.otherElements.matching(identifier: "BrowserWebViewSurface") + XCTAssertEqual( + browserSurfaces.count, + 1, + "Shared BrowserWebViewSurface fallback is only safe when exactly one browser surface exists" + ) + let browserSurface = browserSurfaces.firstMatch XCTAssertTrue(browserSurface.waitForExistence(timeout: timeout), "Expected browser pane content for click target") browserSurface.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).click() RunLoop.current.run(until: Date().addingTimeInterval(0.15)) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cmuxUITests/BrowserPaneNavigationKeybindUITests.swift` around lines 1267 - 1280, The clickBrowserPane helper falls back to a global BrowserWebViewSurface.firstMatch which ignores the requested panelId and makes clicks nondeterministic; update the fallback to scope the webview search to the requested panelId (e.g., find the BrowserPanelContent.\(panelId) element and then look for its descendant with accessibility identifier "BrowserWebViewSurface" or use a panel-scoped identifier like "BrowserWebViewSurface.\(panelId)"), use that scoped element for the click and for the XCTAssertTrue check so the fallback remains panel-specific and deterministic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@cmuxUITests/BrowserPaneNavigationKeybindUITests.swift`:
- Around line 1407-1427: The helper shouldSkipOnCI currently treats any
non-empty CI as true; update it so it only skips for GitHub-hosted heuristics
(GITHUB_ACTIONS == "true", NSHomeDirectory starts with "/Users/runner",
currentDirectoryPath contains "/runner/work/" or starts with "/Users/runner/")
and remove the generic CI fallback, or replace it with an explicit opt-in env
var (e.g., SKIP_UI_TESTS or CMUX_SKIP_UI_TESTS) that must be set to "1"/"true"
to skip; locate the function shouldSkipOnCI and either delete the guard that
checks environment["CI"] or change that branch to read and validate a namespaced
opt-in variable (trim, lowercased, accept only "1"/"true"/"yes") so
self-hosted/UTM VMs are not inadvertently skipped.
In `@cmuxUITests/MenuKeyEquivalentRoutingUITests.swift`:
- Around line 126-130: The test currently treats the precondition check
(reachedOnce from waitForGotoSplitMatch) as an XCTSkip which hides routing
regressions; replace the skip with a test failure so the replay assertion still
runs: change the throw XCTSkip(...) branch that checks reachedOnce into an
XCTFail("Cmd+E did not reach browser content on this CI runner") (do not
return/throw afterwards) so execution continues to the replay assertion and the
regression will surface; keep references to waitForGotoSplitMatch and
reachedOnce as the same condition.
In `@Sources/Panels/MarkdownPanel.swift`:
- Around line 1620-1629: The closure passed to
VncWebSocketProxyBridge.start(remoteHost:remotePort:...) can receive events from
stale bridge instances and overwrite the current proxyBridge; capture the local
bridge in the closure (e.g., let localBridge = bridge) and at the start of the
event handler guard that self?.proxyBridge === localBridge, returning early if
not, so only events from the currently-active bridge are processed; apply the
same capture-and-guard pattern for the other start(...) call handling at the
1821-1851 region to prevent stale-bridge events from affecting the replacement
bridge.
- Around line 1334-1343: InputTelemetry currently stores raw typed text in the
lastText property which exposes sensitive input via automation/state; remove the
lastText field (and any other storage of raw text at the other occurrences
referenced around lines 1473-1475 and 1935-1946) or change the implementation to
only record non-sensitive metadata (counts/timestamps) and gate any raw text
capture behind a strict fake-native UI-test flag (e.g., an explicit
isUITestFakeNative check) so production telemetry never contains plaintext user
input; update InputTelemetry and any places that write/read lastText to use the
safe metadata-only fields or the gated path.
- Around line 500-503: The NSError created with domain "cmux.vnc.proxy" and
userInfo [NSLocalizedDescriptionKey: "Invalid remote VNC port."] is using an
English literal; replace that raw string with a localized string using
String(localized: "vnc.invalidRemotePort", defaultValue: "Invalid remote VNC
port.") (or map to a stable error code enum that Swift maps to a localized
message) so lastErrorDetail receives a localized value; update the same pattern
where NSLocalizedDescriptionKey is set (the other similar throws near the other
VNC error sites) to use String(localized:..., defaultValue:...) or centralized
error-code→localized-string mapping for all occurrences.
- Around line 2026-2028: The code hotlinks noVNC's rfb.js and passes credentials
into its RFB constructor and internal methods (_requestRemoteResize,
_screenSize), which risks credential exfiltration and fragility; replace the
remote import with a vendored, pinned noVNC module shipped with the app and
change the script import to a relative local path (import RFB from
"./vendor/noVNC/core/rfb.js"), bundle/serve that file from the app assets, and
update all uses of RFB/sendCredentials/_requestRemoteResize/_screenSize to use
documented public APIs (or implement resize behavior in the surrounding code) so
you no longer patch underscored internals or call into third‑party code hosted
on another domain. Ensure the vendored copy is versioned/pinned and audited for
security before shipping.
- Around line 307-353: VncSSHHostAliasResolver.resolve(host:) runs a blocking
Process (ssh -G) and is called synchronously from `@MainActor` VncPanel, which can
freeze the UI; make resolution asynchronous and off-main: change the resolver to
an async/nonisolated API (or provide a completion handler) that launches the
Process on a background Task/DispatchQueue, avoid waitUntilExit by using
asynchronous reading of pipes and/or a timeout with explicit process.terminate()
to bound lifetime, and only hop back to the MainActor to apply the returned
candidate to VncPanel; keep logic that parses output and checks
process.terminationStatus but perform it off the main actor and ensure
errors/timeouts return nil.
In `@Sources/Panels/MarkdownPanelView.swift`:
- Around line 549-552: The current onChange handler calls panel.focus() whenever
panel.connectionState becomes .connected, which can steal keyboard focus; update
the handler (the onChange(of: panel.connectionState) block) to only call
panel.focus() when this panel is already the active/visible one (e.g., check a
focus/active flag such as panel.isFocused or panel.isActive) so that the call to
panel.focus() is gated (if state == .connected && panel.isFocused {
panel.focus() } or equivalent). Ensure you rely on the existing explicit focus
paths (onAppear / onChange(of: isFocused)) and do not change those.
- Around line 607-610: The host label inside the ForEach (iterating
panel.discoveredTargets) and Button currently uses a hard-coded string
interpolation "\(target.name) (\(target.endpoint))"; replace this with a
localized format using String(localized:..., defaultValue:...) so the
punctuation/ordering can be adapted for other locales and keep the variable
endpoint inserted via interpolation; update the Button title construction and
ensure the same localized format is used wherever the discovered-host label
appears and that the action still calls
panel.chooseEndpointSuggestion(target.endpoint).
- Around line 496-499: The overlay currently uses MarkdownPointerObserver which
only forwards left-button events and swallows other buttons; update this to use
a VNC-aware observer (e.g., VNCPointerObserver) or extend
MarkdownPointerObserver to forward the full mouse-button path so non-left clicks
reach the native VNC surface; specifically modify the overlay code that
instantiates MarkdownPointerObserver (and the observer implementation) to handle
all NSEvent/NSMouseButton types and pass them through or forward them to
onRequestPanelFocus (or the VNC input pipeline) instead of blocking them.
- Around line 701-715: connectOrDisconnect() currently bypasses the same
validation used by the UI button; before performing any action (connect,
submitCredentials, disconnect) check the existing isConnectButtonDisabled gate
and return early if it is true so keyboard Submit cannot trigger connect/submit
when fields are invalid. Specifically, at the top of connectOrDisconnect() call
the same boolean check (referencing isConnectButtonDisabled) and only proceed to
call panel.connect(), panel.submitCredentials(), panel.disconnect() when the
gate allows it; preserve the existing short-circuit logic for panel.isConnected
and panel.connectionState == .connecting but ensure the disabled-state check
prevents panel.connect() and panel.submitCredentials() from running.
In `@Sources/TabManager.swift`:
- Around line 5385-5401: The current code sets selectedTabId before attempting
to create the VNC surface, causing the UI to switch to tabId even if
newVncSurface(inPane:..., endpoint:..., autoConnect:..., focus:...,
insertAtEnd:...) returns nil; move the selectedTabId = tabId assignment so it
happens only after workspace.newVncSurface succeeds (i.e., after vncPanel is
non-nil), then call rememberFocusedSurface(tabId:tabId, surfaceId:vncPanel.id)
and return vncPanel.id, leaving tabs and workspace lookup logic unchanged.
In `@Sources/TerminalController.swift`:
- Around line 6184-6191: Remove the raw "input_last_text" value from the
socket/state payload generation: do not emit vncPanel.inputLastTextForAutomation
(the "input_last_text" key) in the payloads assembled in TerminalController;
instead emit non-sensitive metadata such as a presence flag or length (e.g.,
"input_last_text_present" or "input_last_text_length") or gate the raw string
behind a DEBUG-only test hook. Update the code paths that use
v2OrNull(vncPanel.inputLastTextForAutomation) to return the safe metadata (or
omit the key entirely) and apply the same change to the other affected block
referenced in the review (lines around 12700-12728); keep references to
vncPanel, v2OrNull, and the "input_last_text" key so you can find and modify the
two payload locations.
- Around line 6141-6173: Resolve and validate the incoming surface_id before
selecting a TabManager/Workspace: call v2UUID(params, "surface_id") up front and
return .err(code: "invalid_params", ...) if it's missing/invalid instead of
falling back to ws.focusedPanelId; then check ownership by seeing if the
resolved surfaceId is in the workspace's panels (ws.panels[surfaceId]) and if
not use AppDelegate.shared?.locateSurface(surfaceId:) to find the correct
(tabManager, workspace) pair and only then bind to that workspace; update uses
of v2ResolveTabManager(params:) and v2ResolveWorkspace(params:tabManager:) so
you only pick a tabManager when it actually owns the surface, and ensure error
responses use "invalid_params" for bad incoming ids and "not_found" when
locateSurface fails before dereferencing vncPanel.
In `@Sources/Workspace.swift`:
- Around line 9274-9280: The VNC panel seeding bypasses the normalization done
in VncPanel.init(workspaceId:endpoint:) and restoreSessionSnapshot(_:): trim the
endpoint and username before assigning to VncPanel (e.g., when creating
VncPanel(workspaceId:endpoint:) and then setting vncPanel.usernameInput and
vncPanel.passwordInput), so call .trimmingCharacters(in:
.whitespacesAndNewlines) on endpoint and username prior to assignment while
leaving passwordInput unchanged; update both identical call sites that set
vncPanel.usernameInput/passwordInput accordingly.
In `@vendor/bonsplit`:
- Line 1: The parent repo's submodule pointer for vendor/bonsplit was updated to
commit 2979ef647d4a6ab0bf7f93ce82c59cbf0c70e1e1 which does not exist on
vendor/bonsplit's origin/main (current submodule HEAD is
5d508dc0773a1cbc62390ca8446cfeaebd8a7d52), so revert the pointer change in the
parent commit until that commit is pushed to the vendor/bonsplit remote; either
push the missing commit to vendor/bonsplit's main branch or reset the submodule
pointer in the parent to an existing commit (e.g.,
5d508dc0773a1cbc62390ca8446cfeaebd8a7d52) and update the submodule cleanly
(using git submodule update --init --recursive) before reopening the PR.
---
Outside diff comments:
In `@cmuxUITests/MenuKeyEquivalentRoutingUITests.swift`:
- Around line 325-344: The helper refocusWebView currently swallows the final
focus checks; change its signature to return a Bool indicating whether the
WKWebView regained first responder and update its internals to return the result
of the final wait predicate (use waitForGotoSplitMatch with the predicate
data["webViewFocusedAfterAddressBarExit"] == "true" rather than discarding it),
and ensure the intermediate escape retry still occurs but its outcome feeds into
the returned Bool; then update every call site to assert the returned Bool (fail
the test if false) so tests require the web view to actually regain focus.
In `@Sources/AppDelegate.swift`:
- Around line 8563-8592: The test can activate a different "fallback" window
because mainTerminalWindow() searches NSApp.windows; change
runSetupWhenWindowReady() to resolve the UI window from the same TabManager
you're about to mutate: use a TabManager-backed window reference (e.g., a
property or method on TabManager such as tabManager.mainWindow or
tabManager.windowForUITests) instead of calling mainTerminalWindow(), and when
making the window key/ordering or calling orderFrontRegardless(), operate on
that TabManager-derived window; you can also update mainTerminalWindow() to
accept a TabManager parameter or add a new helper that prefers tabManager's
window to avoid the XCTest fallback race.
In `@Sources/TerminalController.swift`:
- Around line 14416-14445: The manual tokenization using args.split(separator: "
") in the TerminalController parsing block breaks quoted credentials (e.g.
--password="two words"); replace the space-splitting logic and per-part handling
with the same option parser used by vncState(_:) so flags with spaces/quotes are
parsed correctly. Locate the parsing block that iterates over parts (the code
using args.split and symbols like parseSplitDirection, panelType, direction,
url, endpoint, username, password, autoConnect) and route the raw args string
into the shared option-parsing routine (the one vncState(_) uses) or reuse the
parsing helper used by newVncSplit/newVncSurface, ensuring quoted values are
preserved and then map the resulting parsed options to
panelType/direction/url/endpoint/username/password/autoConnect. Ensure both
occurrences (this block and the similar block around newVncSplit/newVncSurface)
are updated to use the same parser.
---
Nitpick comments:
In `@cmuxUITests/BrowserPaneNavigationKeybindUITests.swift`:
- Around line 1379-1390: The helper ensureForegroundAfterLaunch currently waits
the full timeout before trying to recover from a .runningBackground launch;
change its logic so you check app.state early and do not burn the full timeout:
first, if app.state == .runningBackground then call app.activate() and
immediately wait a short recovery timeout (e.g., 1–2s) for .runningForeground;
otherwise try a short initial wait for .runningForeground and only if that fails
and state is .runningBackground call activate() and wait a slightly longer
recovery window; update the function ensureForegroundAfterLaunch to use these
shorter/fallback waits to avoid waiting the entire timeout before calling
activate().
- Around line 1267-1280: The clickBrowserPane helper falls back to a global
BrowserWebViewSurface.firstMatch which ignores the requested panelId and makes
clicks nondeterministic; update the fallback to scope the webview search to the
requested panelId (e.g., find the BrowserPanelContent.\(panelId) element and
then look for its descendant with accessibility identifier
"BrowserWebViewSurface" or use a panel-scoped identifier like
"BrowserWebViewSurface.\(panelId)"), use that scoped element for the click and
for the XCTAssertTrue check so the fallback remains panel-specific and
deterministic.
In `@Sources/AppDelegate.swift`:
- Around line 8594-8612: Extract the repeated four
KeyboardShortcutSettings.setShortcut calls (the StoredShortcut(...) entries for
.focusLeft, .focusRight, .focusUp, .focusDown) into a single helper function
(e.g., seedGotoSplitShortcuts or applyGotoSplitShortcuts) and call that helper
both here (inside the if !useGhosttyConfig block) and at the other setup
location where they are currently duplicated; the helper should encapsulate
creation of the StoredShortcut instances and the four setShortcut calls so
future changes stay in one place.
In `@Sources/Workspace.swift`:
- Around line 451-452: Add a unit test that exercises the VNC session snapshot
and rehydrate path by creating a VNC session, calling sessionSnapshot(), then
calling restoreSessionSnapshot() and asserting the restored
SessionVncPanelSnapshot fields (endpoint, username, customTitle, autoConnect)
match the original; update test coverage to include the vncSnapshot property
alongside markdownSnapshot and ensure the restore path uses the
SessionVncPanelSnapshot deserialization (check functions/session paths that
reference vncSnapshot and restoreSessionSnapshot); repeat the same round-trip
test pattern for other similar spots referenced (around the blocks at the other
occurrences) so endpoint/username/title/auto-connect are validated end-to-end.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: edcb8d6d-d304-48f0-80a9-2e40d2692770
📒 Files selected for processing (17)
Resources/Localizable.xcstringsSources/AppDelegate.swiftSources/CmuxConfig.swiftSources/ContentView.swiftSources/Panels/MarkdownPanel.swiftSources/Panels/MarkdownPanelView.swiftSources/Panels/Panel.swiftSources/Panels/PanelContentView.swiftSources/SessionPersistence.swiftSources/TabManager.swiftSources/TerminalController.swiftSources/Workspace.swiftSources/WorkspaceContentView.swiftcmuxUITests/BrowserPaneNavigationKeybindUITests.swiftcmuxUITests/MenuKeyEquivalentRoutingUITests.swiftcmuxUITests/VncPanelInputRoutingUITests.swiftvendor/bonsplit
| private func shouldSkipOnCI(environment: [String: String]) -> Bool { | ||
| if environment["GITHUB_ACTIONS"]?.lowercased() == "true" { | ||
| return true | ||
| } | ||
|
|
||
| if NSHomeDirectory().hasPrefix("/Users/runner") { | ||
| return true | ||
| } | ||
|
|
||
| let currentPath = FileManager.default.currentDirectoryPath | ||
| if currentPath.hasPrefix("/Users/runner/") || currentPath.contains("/runner/work/") { | ||
| return true | ||
| } | ||
|
|
||
| guard let ci = environment["CI"]?.trimmingCharacters(in: .whitespacesAndNewlines), | ||
| !ci.isEmpty else { | ||
| return false | ||
| } | ||
|
|
||
| let normalized = ci.lowercased() | ||
| return normalized != "0" && normalized != "false" && normalized != "no" |
There was a problem hiding this comment.
Don’t skip these tests on every CI environment.
The generic CI fallback means any non-empty CI value skips the test, so self-hosted or UTM VM runs lose the same end-to-end coverage that this repo still expects to keep. The reasons at the call sites are GitHub-hosted-runner-specific, so this helper should stay scoped to those heuristics or use an explicit opt-in env var for broader suppression.
Suggested narrowing
private func shouldSkipOnCI(environment: [String: String]) -> Bool {
if environment["GITHUB_ACTIONS"]?.lowercased() == "true" {
return true
}
if NSHomeDirectory().hasPrefix("/Users/runner") {
return true
}
let currentPath = FileManager.default.currentDirectoryPath
if currentPath.hasPrefix("/Users/runner/") || currentPath.contains("/runner/work/") {
return true
}
- guard let ci = environment["CI"]?.trimmingCharacters(in: .whitespacesAndNewlines),
- !ci.isEmpty else {
- return false
- }
-
- let normalized = ci.lowercased()
- return normalized != "0" && normalized != "false" && normalized != "no"
+ return environment["CMUX_SKIP_FLAKY_UI_TESTS"] == "1"
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@cmuxUITests/BrowserPaneNavigationKeybindUITests.swift` around lines 1407 -
1427, The helper shouldSkipOnCI currently treats any non-empty CI as true;
update it so it only skips for GitHub-hosted heuristics (GITHUB_ACTIONS ==
"true", NSHomeDirectory starts with "/Users/runner", currentDirectoryPath
contains "/runner/work/" or starts with "/Users/runner/") and remove the generic
CI fallback, or replace it with an explicit opt-in env var (e.g., SKIP_UI_TESTS
or CMUX_SKIP_UI_TESTS) that must be set to "1"/"true" to skip; locate the
function shouldSkipOnCI and either delete the guard that checks
environment["CI"] or change that branch to read and validate a namespaced opt-in
variable (trim, lowercased, accept only "1"/"true"/"yes") so self-hosted/UTM VMs
are not inadvertently skipped.
| let reachedOnce = waitForGotoSplitMatch(timeout: 5.0) { data in | ||
| data["browserPageTitle"] == "cmde-1" | ||
| } | ||
| if !reachedOnce { | ||
| throw XCTSkip("Cmd+E did not reach browser content on this CI runner") |
There was a problem hiding this comment.
Don't skip the regression this test is meant to catch.
If the page never sees Cmd+E, or jumps straight to cmde-2, this now exits as skipped and never executes the replay assertion. That turns a real routing regression into a green run.
🧪 Keep the precondition as a failure
let reachedOnce = waitForGotoSplitMatch(timeout: 5.0) { data in
data["browserPageTitle"] == "cmde-1"
}
- if !reachedOnce {
- throw XCTSkip("Cmd+E did not reach browser content on this CI runner")
- }
+ guard reachedOnce else {
+ XCTFail("Expected Cmd+E to reach browser content before checking for replay. data=\(loadGotoSplit() ?? [:])")
+ return
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@cmuxUITests/MenuKeyEquivalentRoutingUITests.swift` around lines 126 - 130,
The test currently treats the precondition check (reachedOnce from
waitForGotoSplitMatch) as an XCTSkip which hides routing regressions; replace
the skip with a test failure so the replay assertion still runs: change the
throw XCTSkip(...) branch that checks reachedOnce into an XCTFail("Cmd+E did not
reach browser content on this CI runner") (do not return/throw afterwards) so
execution continues to the replay assertion and the regression will surface;
keep references to waitForGotoSplitMatch and reachedOnce as the same condition.
| @@ -1 +1 @@ | |||
| Subproject commit 098d9fa00e2b1d4712f1a46b818ee7d53d4aa31f | |||
| Subproject commit 2979ef647d4a6ab0bf7f93ce82c59cbf0c70e1e1 | |||
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Confirm parent repo records the intended submodule SHA
git submodule status -- vendor/bonsplit
# Inspect submodule HEAD and branch state
git -C vendor/bonsplit rev-parse HEAD
git -C vendor/bonsplit branch --show-current || true
# Verify the referenced commit is ancestor of origin/main (expected: exit 0)
git -C vendor/bonsplit fetch origin main --quiet
git -C vendor/bonsplit merge-base --is-ancestor 2979ef647d4a6ab0bf7f93ce82c59cbf0c70e1e1 origin/main
echo "OK: submodule commit is reachable from origin/main"Repository: manaflow-ai/cmux
Length of output: 349
Revert submodule pointer update until commit is pushed to vendor/bonsplit remote.
The submodule pointer points to commit 2979ef647d4a6ab0bf7f93ce82c59cbf0c70e1e1, which does not exist on origin/main in vendor/bonsplit (error: not our ref). The submodule is in detached HEAD state, and the current HEAD is 5d508dc0773a1cbc62390ca8446cfeaebd8a7d52, not the commit being referenced. This will break clones and CI for collaborators. Push the commit to the submodule's remote main branch first, then update the parent pointer.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@vendor/bonsplit` at line 1, The parent repo's submodule pointer for
vendor/bonsplit was updated to commit 2979ef647d4a6ab0bf7f93ce82c59cbf0c70e1e1
which does not exist on vendor/bonsplit's origin/main (current submodule HEAD is
5d508dc0773a1cbc62390ca8446cfeaebd8a7d52), so revert the pointer change in the
parent commit until that commit is pushed to the vendor/bonsplit remote; either
push the missing commit to vendor/bonsplit's main branch or reset the submodule
pointer in the parent to an existing commit (e.g.,
5d508dc0773a1cbc62390ca8446cfeaebd8a7d52) and update the submodule cleanly
(using git submodule update --init --recursive) before reopening the PR.
There was a problem hiding this comment.
6 issues found across 17 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="cmuxUITests/BrowserPaneNavigationKeybindUITests.swift">
<violation number="1" location="cmuxUITests/BrowserPaneNavigationKeybindUITests.swift:1380">
P2: `ensureForegroundAfterLaunch` waits for foreground before attempting activation, so background launches can incur an unnecessary full timeout per test.</violation>
</file>
<file name="Sources/Panels/MarkdownPanelView.swift">
<violation number="1" location="Sources/Panels/MarkdownPanelView.swift:444">
P2: Do not disable the connect button during `.connecting`; the action handler already uses that state to cancel/disconnect, and disabling the button makes that recovery path unreachable.</violation>
<violation number="2" location="Sources/Panels/MarkdownPanelView.swift:551">
P2: Only auto-focus the VNC view on connect when this panel is focused. The current code can steal first responder from another active pane when a background VNC connection finishes.</violation>
</file>
<file name="cmuxUITests/MenuKeyEquivalentRoutingUITests.swift">
<violation number="1" location="cmuxUITests/MenuKeyEquivalentRoutingUITests.swift:330">
P2: Assert that WebKit focus restoration succeeded; otherwise these shortcut tests can pass without exercising the intended first-responder state.</violation>
</file>
<file name="Sources/Panels/MarkdownPanel.swift">
<violation number="1" location="Sources/Panels/MarkdownPanel.swift:1610">
P2: Resolve SSH host aliases off the main actor. Running `ssh -G` synchronously in `connect()` can freeze the UI while connecting.</violation>
<violation number="2" location="Sources/Panels/MarkdownPanel.swift:2027">
P1: Avoid loading the core VNC script from a remote URL. Vendoring/pinning noVNC locally is needed to prevent supply-chain exposure and runtime breakage.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| <body> | ||
| <div id="screen" tabindex="0"></div> | ||
| <script type="module" crossorigin="anonymous"> | ||
| import RFB from "https://novnc.com/noVNC/core/rfb.js"; |
There was a problem hiding this comment.
P1: Avoid loading the core VNC script from a remote URL. Vendoring/pinning noVNC locally is needed to prevent supply-chain exposure and runtime breakage.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/Panels/MarkdownPanel.swift, line 2027:
<comment>Avoid loading the core VNC script from a remote URL. Vendoring/pinning noVNC locally is needed to prevent supply-chain exposure and runtime breakage.</comment>
<file context>
@@ -182,3 +185,2042 @@ final class MarkdownPanel: Panel, ObservableObject {
+ <body>
+ <div id="screen" tabindex="0"></div>
+ <script type="module" crossorigin="anonymous">
+ import RFB from "https://novnc.com/noVNC/core/rfb.js";
+ (() => {
+ const handler = (() => {
</file context>
| if app.wait(for: .runningForeground, timeout: timeout) { | ||
| return true | ||
| } | ||
| if app.state == .runningBackground { | ||
| // App launched but couldn't activate — continue in background. | ||
| // XCUIElement queries and keyboard input work through the | ||
| // accessibility framework regardless of activation state. | ||
| return | ||
| app.activate() | ||
| if app.wait(for: .runningForeground, timeout: 6.0) { | ||
| return true | ||
| } | ||
| } | ||
|
|
||
| XCTFail("App failed to start. state=\(app.state.rawValue)") | ||
| app.activate() | ||
| return app.wait(for: .runningForeground, timeout: 2.0) |
There was a problem hiding this comment.
P2: ensureForegroundAfterLaunch waits for foreground before attempting activation, so background launches can incur an unnecessary full timeout per test.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cmuxUITests/BrowserPaneNavigationKeybindUITests.swift, line 1380:
<comment>`ensureForegroundAfterLaunch` waits for foreground before attempting activation, so background launches can incur an unnecessary full timeout per test.</comment>
<file context>
@@ -1345,27 +1369,25 @@ final class BrowserPaneNavigationKeybindUITests: XCTestCase {
+ }
+ private func ensureForegroundAfterLaunch(_ app: XCUIApplication, timeout: TimeInterval) -> Bool {
+ if app.wait(for: .runningForeground, timeout: timeout) {
+ return true
+ }
</file context>
| if app.wait(for: .runningForeground, timeout: timeout) { | |
| return true | |
| } | |
| if app.state == .runningBackground { | |
| // App launched but couldn't activate — continue in background. | |
| // XCUIElement queries and keyboard input work through the | |
| // accessibility framework regardless of activation state. | |
| return | |
| app.activate() | |
| if app.wait(for: .runningForeground, timeout: 6.0) { | |
| return true | |
| } | |
| } | |
| XCTFail("App failed to start. state=\(app.state.rawValue)") | |
| app.activate() | |
| return app.wait(for: .runningForeground, timeout: 2.0) | |
| if app.state == .runningForeground { | |
| return true | |
| } | |
| if app.state == .runningBackground { | |
| app.activate() | |
| if app.wait(for: .runningForeground, timeout: 6.0) { | |
| return true | |
| } | |
| } else if app.wait(for: .runningForeground, timeout: timeout) { | |
| return true | |
| } | |
| app.activate() | |
| return app.wait(for: .runningForeground, timeout: 2.0) |
| }, | ||
| "Expected Cmd+L to focus omnibar (WebKit not first responder)" | ||
| ) | ||
| _ = waitForGotoSplitMatch(timeout: 2.5) { data in |
There was a problem hiding this comment.
P2: Assert that WebKit focus restoration succeeded; otherwise these shortcut tests can pass without exercising the intended first-responder state.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cmuxUITests/MenuKeyEquivalentRoutingUITests.swift, line 330:
<comment>Assert that WebKit focus restoration succeeded; otherwise these shortcut tests can pass without exercising the intended first-responder state.</comment>
<file context>
@@ -318,22 +322,14 @@ final class MenuKeyEquivalentRoutingUITests: XCTestCase {
- },
- "Expected Cmd+L to focus omnibar (WebKit not first responder)"
- )
+ _ = waitForGotoSplitMatch(timeout: 2.5) { data in
+ data["webViewFocusedAfterAddressBarFocus"] == "false"
+ }
</file context>
There was a problem hiding this comment.
♻️ Duplicate comments (2)
Sources/Panels/MarkdownPanel.swift (2)
311-313:⚠️ Potential issue | 🟠 MajorAdd timeout + termination handling for
ssh -Galias resolution.The connect flow awaits
VncSSHHostAliasResolver.resolve, butresolveBlockinguseswaitUntilExit()with no timeout. Ifssh -Ghangs, the connect attempt can stall indefinitely.Suggested fix
private static func resolveBlocking(host: String) -> String? { @@ do { try process.run() } catch { return nil } - process.waitUntilExit() + let exitSignal = DispatchSemaphore(value: 0) + process.terminationHandler = { _ in + exitSignal.signal() + } + let waitResult = exitSignal.wait(timeout: .now() + 3.0) + if waitResult == .timedOut { + process.terminate() + _ = exitSignal.wait(timeout: .now() + 0.2) + return nil + } guard process.terminationStatus == 0 else { return nil }#!/bin/bash # Verify whether VncSSHHostAliasResolver currently has bounded process lifetime handling. # Expected: `waitUntilExit` present and no nearby timeout/termination guard => confirms risk. rg -n -C4 'enum VncSSHHostAliasResolver|Task\.detached|waitUntilExit|terminationHandler|terminate\(' Sources/Panels/MarkdownPanel.swiftAlso applies to: 341-342
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Panels/MarkdownPanel.swift` around lines 311 - 313, VncSSHHostAliasResolver.resolveBlocking currently calls Process.waitUntilExit() with no timeout, so update resolveBlocking (and any similar call sites used by VncSSHHostAliasResolver.resolve) to enforce a bounded lifetime: start the ssh -G Process, schedule a timeout (e.g., DispatchQueue or Task.sleep) and if the process hasn’t exited by deadline call process.terminate(), then if still running forcefully kill and treat as failure; ensure you capture the termination status/error and return/fail promptly to the awaiting caller (resolve) instead of blocking forever; reference functions: VncSSHHostAliasResolver, resolveBlocking, resolve, and usages of waitUntilExit() and terminationHandler when locating the code to change.
1361-1370:⚠️ Potential issue | 🔴 CriticalRemove plaintext keystroke capture from VNC telemetry.
Line 1369 stores raw typed text and Line 1500 exposes it through automation state. This can leak secrets (passwords, tokens) entered in the remote desktop.
Suggested fix
private struct InputTelemetry { var keyDownCount: Int = 0 var modifiedKeyDownCount: Int = 0 var textInputCount: Int = 0 var mouseDownCount: Int = 0 var mouseUpCount: Int = 0 var mouseDraggedCount: Int = 0 var scrollCount: Int = 0 - var lastText: String = "" } @@ - var inputLastTextForAutomation: String? { - inputTelemetry.lastText.isEmpty ? nil : inputTelemetry.lastText - } + var inputLastTextForAutomation: String? { nil } @@ case .text(let text): inputTelemetry.textInputCount += 1 - if !text.isEmpty { - inputTelemetry.lastText = text - } + _ = textAlso applies to: 1500-1502, 1991-1995
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Panels/MarkdownPanel.swift` around lines 1361 - 1370, The InputTelemetry struct currently captures plaintext user input via the lastText property and that plaintext is later exposed in automation state; remove this sensitive data capture by deleting the lastText property from InputTelemetry, remove any assignments that set lastText (e.g., in input handling code that updates InputTelemetry.lastText), and remove any code that exposes the plaintext through automation/state APIs (e.g., the automation state accessor or serialization that returns lastText). If you still need telemetry about typing, replace plaintext with non-sensitive alternatives such as counts (textInputCount), a boolean flag indicating recent typing, or an integer lastTextLength or a hashed/entropy-only metric; update the code paths that previously consumed lastText to use the new non-sensitive field instead (e.g., where automation state previously read lastText).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@Sources/Panels/MarkdownPanel.swift`:
- Around line 311-313: VncSSHHostAliasResolver.resolveBlocking currently calls
Process.waitUntilExit() with no timeout, so update resolveBlocking (and any
similar call sites used by VncSSHHostAliasResolver.resolve) to enforce a bounded
lifetime: start the ssh -G Process, schedule a timeout (e.g., DispatchQueue or
Task.sleep) and if the process hasn’t exited by deadline call
process.terminate(), then if still running forcefully kill and treat as failure;
ensure you capture the termination status/error and return/fail promptly to the
awaiting caller (resolve) instead of blocking forever; reference functions:
VncSSHHostAliasResolver, resolveBlocking, resolve, and usages of waitUntilExit()
and terminationHandler when locating the code to change.
- Around line 1361-1370: The InputTelemetry struct currently captures plaintext
user input via the lastText property and that plaintext is later exposed in
automation state; remove this sensitive data capture by deleting the lastText
property from InputTelemetry, remove any assignments that set lastText (e.g., in
input handling code that updates InputTelemetry.lastText), and remove any code
that exposes the plaintext through automation/state APIs (e.g., the automation
state accessor or serialization that returns lastText). If you still need
telemetry about typing, replace plaintext with non-sensitive alternatives such
as counts (textInputCount), a boolean flag indicating recent typing, or an
integer lastTextLength or a hashed/entropy-only metric; update the code paths
that previously consumed lastText to use the new non-sensitive field instead
(e.g., where automation state previously read lastText).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ec3f2afa-5d76-487a-a3ee-e128243a5979
📒 Files selected for processing (5)
Resources/Localizable.xcstringsSources/ContentView.swiftSources/Panels/MarkdownPanel.swiftSources/Panels/MarkdownPanelView.swiftSources/SessionPersistence.swift
✅ Files skipped from review due to trivial changes (3)
- Sources/Panels/MarkdownPanelView.swift
- Sources/ContentView.swift
- Resources/Localizable.xcstrings
🚧 Files skipped from review as they are similar to previous changes (1)
- Sources/SessionPersistence.swift
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
Sources/Workspace.swift (1)
9251-9330: Consider extracting shared VNC bootstrap logic to reduce drift risk.
newVncSplit(...)andnewVncSurface(...)duplicate panel seeding (trim, username/password assignment, subscription install, optional autoconnect). A small helper would make future fixes less error-prone.♻️ Optional refactor sketch
+ private func makeVncPanel( + endpoint: String?, + username: String?, + password: String? + ) -> VncPanel { + let trimmedEndpoint = endpoint?.trimmingCharacters(in: .whitespacesAndNewlines) + let trimmedUsername = username?.trimmingCharacters(in: .whitespacesAndNewlines) + let panel = VncPanel(workspaceId: id, endpoint: trimmedEndpoint) + if let trimmedUsername, !trimmedUsername.isEmpty { panel.usernameInput = trimmedUsername } + if let password { panel.passwordInput = password } + panels[panel.id] = panel + panelTitles[panel.id] = panel.displayTitle + installVncPanelSubscription(panel) + return panel + }Also applies to: 9332-9400
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Workspace.swift` around lines 9251 - 9330, newVncSplit(...) and newVncSurface(...) both duplicate VncPanel seeding logic (trimming endpoint/username, assigning usernameInput/passwordInput, storing in panels/panelTitles/surfaceIdToPanelId, calling installVncPanelSubscription and handling autoConnect); extract this into a single helper like seedVncPanel(endpoint:username:password:autoConnect:surfaceId:) that returns the created VncPanel and performs: trim inputs, init VncPanel(workspaceId: id, endpoint:), set usernameInput/passwordInput if present, insert into panels[vncPanel.id], panelTitles[vncPanel.id], surfaceIdToPanelId[newTab.id] (accept surfaceId param), call installVncPanelSubscription(vncPanel) and schedule autoConnect via DispatchQueue.main.async, then update newVncSplit and newVncSurface to call that helper and remove the duplicated code.Sources/Panels/MarkdownPanelView.swift (1)
472-859: Consider splitting the VNC view out ofMarkdownPanelView.swift.This file now owns markdown UI, VNC UI, shared pointer plumbing, and two representables. Moving
VncPanelViewinto its own file would make the panel boundaries much easier to reason about and review.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Panels/MarkdownPanelView.swift` around lines 472 - 859, The VncPanelView and its two NSViewRepresentable helpers (VncPanelNativeSessionRepresentable, VncPanelWebViewRepresentable) should be extracted from the large MarkdownPanelView.swift into a new source file so the VNC UI is isolated: create a new Swift file containing struct VncPanelView, the private enum Field, and the two representables, keep the same types/signatures (VncPanelView, VncPanelNativeSessionRepresentable, VncPanelWebViewRepresentable) and preserve all referenced state/props (panel, isFocused, isVisibleInUI, portalPriority, onRequestPanelFocus) and methods (connectOrDisconnect, focusMostUsefulField, triggerFocusFlashAnimation, focusFlashAnimation) but import or make accessible any shared symbols used by the VNC code (FocusFlashPattern, cmuxAccentColor(), MarkdownPointerObserver, CmuxWebView, etc.)—adjust access control if needed and remove the moved definitions from the original file, then build to fix any missing imports or visibility issues.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Sources/Panels/MarkdownPanelView.swift`:
- Around line 502-514: The connect button can be disabled while a connection is
in progress because isConnectButtonDisabled falls through to endpoint
validation; add an early check for the connecting/cancel path (e.g., if
panel.isConnecting { return false }) at the top of isConnectButtonDisabled so
the UI keeps the visible cancel action enabled (remember connectOrDisconnect()
treats .connecting as the cancel/disconnect case).
- Around line 795-810: The focusMostUsefulField() currently always sets
focusedField = .password after checking username, which ignores
panel.requiredCredentialFields; instead, when panel.isAwaitingCredentials is
true, inspect panel.requiredCredentialFields in order and set focusedField to
the first required credential whose corresponding input (e.g.,
panel.usernameInput, panel.passwordInput) is empty; if none are empty, leave
focus or default to .endpoint. Update focusMostUsefulField() to use
panel.requiredCredentialFields and the inputs to pick the next missing required
field rather than unconditionally assigning .password.
---
Nitpick comments:
In `@Sources/Panels/MarkdownPanelView.swift`:
- Around line 472-859: The VncPanelView and its two NSViewRepresentable helpers
(VncPanelNativeSessionRepresentable, VncPanelWebViewRepresentable) should be
extracted from the large MarkdownPanelView.swift into a new source file so the
VNC UI is isolated: create a new Swift file containing struct VncPanelView, the
private enum Field, and the two representables, keep the same types/signatures
(VncPanelView, VncPanelNativeSessionRepresentable, VncPanelWebViewRepresentable)
and preserve all referenced state/props (panel, isFocused, isVisibleInUI,
portalPriority, onRequestPanelFocus) and methods (connectOrDisconnect,
focusMostUsefulField, triggerFocusFlashAnimation, focusFlashAnimation) but
import or make accessible any shared symbols used by the VNC code
(FocusFlashPattern, cmuxAccentColor(), MarkdownPointerObserver, CmuxWebView,
etc.)—adjust access control if needed and remove the moved definitions from the
original file, then build to fix any missing imports or visibility issues.
In `@Sources/Workspace.swift`:
- Around line 9251-9330: newVncSplit(...) and newVncSurface(...) both duplicate
VncPanel seeding logic (trimming endpoint/username, assigning
usernameInput/passwordInput, storing in panels/panelTitles/surfaceIdToPanelId,
calling installVncPanelSubscription and handling autoConnect); extract this into
a single helper like
seedVncPanel(endpoint:username:password:autoConnect:surfaceId:) that returns the
created VncPanel and performs: trim inputs, init VncPanel(workspaceId: id,
endpoint:), set usernameInput/passwordInput if present, insert into
panels[vncPanel.id], panelTitles[vncPanel.id], surfaceIdToPanelId[newTab.id]
(accept surfaceId param), call installVncPanelSubscription(vncPanel) and
schedule autoConnect via DispatchQueue.main.async, then update newVncSplit and
newVncSurface to call that helper and remove the duplicated code.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 64ac9465-4ef8-446c-886d-8993098a47f5
📒 Files selected for processing (6)
Resources/Localizable.xcstringsSources/Panels/MarkdownPanel.swiftSources/Panels/MarkdownPanelView.swiftSources/TabManager.swiftSources/TerminalController.swiftSources/Workspace.swift
✅ Files skipped from review due to trivial changes (2)
- Sources/TabManager.swift
- Resources/Localizable.xcstrings
🚧 Files skipped from review as they are similar to previous changes (1)
- Sources/Panels/MarkdownPanel.swift
| private var isConnectButtonDisabled: Bool { | ||
| if panel.isConnected { | ||
| return false | ||
| } | ||
| if panel.isAwaitingCredentials { | ||
| let usernameReady = !panel.requiredCredentialFields.contains(.username) || | ||
| !panel.usernameInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty | ||
| let passwordReady = !panel.requiredCredentialFields.contains(.password) || | ||
| !panel.passwordInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty | ||
| return !(usernameReady && passwordReady) | ||
| } | ||
| return panel.endpointInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty | ||
| } |
There was a problem hiding this comment.
Keep the button enabled for the .connecting cancel path.
connectOrDisconnect() treats .connecting as a disconnect/cancel action, but isConnectButtonDisabled still falls through to endpoint validation. If the user edits or clears the endpoint mid-connect, the visible cancel button can become disabled until the attempt times out.
Suggested fix
private var isConnectButtonDisabled: Bool {
if panel.isConnected {
return false
}
if panel.isAwaitingCredentials {
let usernameReady = !panel.requiredCredentialFields.contains(.username) ||
!panel.usernameInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
let passwordReady = !panel.requiredCredentialFields.contains(.password) ||
!panel.passwordInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
return !(usernameReady && passwordReady)
}
+ if panel.connectionState == .connecting {
+ return false
+ }
return panel.endpointInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/Panels/MarkdownPanelView.swift` around lines 502 - 514, The connect
button can be disabled while a connection is in progress because
isConnectButtonDisabled falls through to endpoint validation; add an early check
for the connecting/cancel path (e.g., if panel.isConnecting { return false }) at
the top of isConnectButtonDisabled so the UI keeps the visible cancel action
enabled (remember connectOrDisconnect() treats .connecting as the
cancel/disconnect case).
| private func focusMostUsefulField() { | ||
| if panel.endpointInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { | ||
| focusedField = .endpoint | ||
| return | ||
| } | ||
| if panel.isAwaitingCredentials { | ||
| if panel.requiredCredentialFields.contains(.username), | ||
| panel.usernameInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { | ||
| focusedField = .username | ||
| return | ||
| } | ||
| focusedField = .password | ||
| return | ||
| } | ||
| focusedField = .endpoint | ||
| } |
There was a problem hiding this comment.
Pick the next required credential field, not password unconditionally.
After the username branch, this always focuses .password. For username-only auth challenges, that jumps the caret into an optional field even when no password is required.
Suggested fix
private func focusMostUsefulField() {
if panel.endpointInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
focusedField = .endpoint
return
}
if panel.isAwaitingCredentials {
if panel.requiredCredentialFields.contains(.username),
panel.usernameInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
focusedField = .username
return
}
- focusedField = .password
+ if panel.requiredCredentialFields.contains(.password) {
+ focusedField = .password
+ return
+ }
+ focusedField = .username
return
}
focusedField = .endpoint
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/Panels/MarkdownPanelView.swift` around lines 795 - 810, The
focusMostUsefulField() currently always sets focusedField = .password after
checking username, which ignores panel.requiredCredentialFields; instead, when
panel.isAwaitingCredentials is true, inspect panel.requiredCredentialFields in
order and set focusedField to the first required credential whose corresponding
input (e.g., panel.usernameInput, panel.passwordInput) is empty; if none are
empty, leave focus or default to .endpoint. Update focusMostUsefulField() to use
panel.requiredCredentialFields and the inputs to pick the next missing required
field rather than unconditionally assigning .password.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7cb7945532
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if usernameTrimmed.isEmpty { | ||
| authComponent = "" | ||
| } else { |
There was a problem hiding this comment.
Include password-only credentials in VNC URI
When usernameInput is blank but passwordInput is provided, this branch forces authComponent to "", so the password is dropped from the connection address. That makes password-only VNC servers fail authentication (or repeatedly prompt) even though the UI presents username as optional and accepts a password. Build the URI userinfo for password-only inputs as well instead of discarding the password.
Useful? React with 👍 / 👎.
| ? "[\(trimmedHost)]" | ||
| : trimmedHost | ||
| let usernameTrimmed = username.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| let passwordTrimmed = password.trimmingCharacters(in: .whitespacesAndNewlines) |
There was a problem hiding this comment.
Preserve whitespace in VNC passwords
The password is trimmed before encoding, which silently changes credentials that intentionally start or end with spaces. For such servers, auth will always fail even if the user enters the correct password in the UI. Passwords should be passed verbatim (only host/endpoint fields should be normalized).
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
cmuxUITests/VncPanelInputRoutingUITests.swift (1)
16-64: Add explicit right/other-button coverage before calling this routing complete.This test only exercises keyboard input plus left click/drag. The new VNC routing work claims right/other mouse-button support too, so that path can regress while this suite still passes. Please add at least one right-click and one
otherMousecase, with matching automation counters/assertions.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cmuxUITests/VncPanelInputRoutingUITests.swift` around lines 16 - 64, The test testVncPanelRoutesKeyboardMultiKeyAndMouseInputInNativeSession only exercises left click/drag; add an explicit right-click and an other-button click using the same content coordinates (e.g., perform a right click on center and an otherMouse click on another coordinate via the content.coordinate(...) helpers) before the final waitForVncState, and then extend the state-check closure to assert the new counters (e.g., check input_right_mouse_down_count/input_right_mouse_up_count and input_other_mouse_down_count/input_other_mouse_up_count are >= 1) alongside the existing mouse/key counters so the test fails if right/other button routing regresses.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Sources/Panels/MarkdownPanel.swift`:
- Around line 1594-1599: The snapshot is using runtime connection state
(isConnected || connectionState == .connecting) instead of the configured
auto-connect flag; change sessionSnapshot() to read and persist a stored
autoConnect property on VncPanel (e.g., add/use a Bool property named
autoConnect on VncPanel) and pass that value into
SessionVncPanelSnapshot(autoConnect:), rather than computing it from isConnected
or connectionState; ensure anywhere the panel configuration is created/updated
(constructors or UI bindings) sets VncPanel.autoConnect so the snapshot reflects
the user-configured setting.
- Around line 1652-1658: The async host-resolution check only guards the
resolution path but not later native session callbacks, so stale
VncNativeSessionController `.connected`/`.disconnected` handlers can overwrite
state; update all places that register or handle native session callbacks (e.g.,
where VncNativeSessionController invokes connection state handlers and in
startNativeConnection and any disconnectFromCurrentSession(suppressUpdate:)
callback paths) to capture the current connectAttemptID and early-return if the
stored connectAttemptID != captured attemptID, effectively gating callback work
to the same connection generation (apply the same guard to the other occurrences
noted around the 1866–1868 and 1919–1943 regions).
- Around line 1099-1110: The code currently trims username and password before
encoding (usernameTrimmed/passwordTrimmed) which strips significant
leading/trailing whitespace and breaks valid VNC credentials; remove the
trimming and use the original username and password values when building
authComponent (and when calling Self.encodeUserInfo) so whitespace is preserved,
and likewise update hasCredentialValue(...) to stop trimming input there; also
apply the same change in the other occurrence referenced (around the
hasCredentialValue block at the 1815–1820 area) so whitespace-only or
space-padded credentials can be submitted and authenticated.
- Around line 228-230: The code accepts endpoints like "host:" or "host:abc" by
treating a malformed explicit port as “no port supplied” and defaulting to 5900;
fix this by explicitly validating the portString when
normalizedRemainder.hasPrefix(":") — ensure portString is non-empty and
Int(portString) succeeds before calling resolvedPort, and if parsing fails
return nil (do not fall through to default port). Update the three locations
that use normalizedRemainder, portString and resolvedPort (the occurrences
around the shown snippet plus the blocks at the other reported ranges) to first
check portString.isEmpty, then let portInt = Int(portString), then let port =
resolvedPort(portInt) else return nil.
- Around line 2054-2057: The JS error "line ..." prefix is not localized; update
the two components.append calls in MarkdownPanel (the branch that appends "line
\(line):\(column)" and the one that appends "line \(line)") to use
String(localized:..., defaultValue:...) like the other VNC error strings so the
UI shows localized builds correctly; pick clear localization keys (e.g. a key
for "line {line}:{column}" and one for "line {line}") and interpolate the
line/column values into the localized strings when calling String(localized:...,
defaultValue:...).
---
Nitpick comments:
In `@cmuxUITests/VncPanelInputRoutingUITests.swift`:
- Around line 16-64: The test
testVncPanelRoutesKeyboardMultiKeyAndMouseInputInNativeSession only exercises
left click/drag; add an explicit right-click and an other-button click using the
same content coordinates (e.g., perform a right click on center and an
otherMouse click on another coordinate via the content.coordinate(...) helpers)
before the final waitForVncState, and then extend the state-check closure to
assert the new counters (e.g., check
input_right_mouse_down_count/input_right_mouse_up_count and
input_other_mouse_down_count/input_other_mouse_up_count are >= 1) alongside the
existing mouse/key counters so the test fails if right/other button routing
regresses.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ad414d0d-e0a1-49d9-b286-c2ea893e6358
📒 Files selected for processing (3)
Sources/Panels/MarkdownPanel.swiftSources/TerminalController.swiftcmuxUITests/VncPanelInputRoutingUITests.swift
🚧 Files skipped from review as they are similar to previous changes (1)
- Sources/TerminalController.swift
| guard normalizedRemainder.hasPrefix(":") else { return nil } | ||
| let portString = String(normalizedRemainder.dropFirst()) | ||
| guard let port = resolvedPort(Int(portString)) else { return nil } |
There was a problem hiding this comment.
Reject malformed explicit ports instead of defaulting them to 5900.
Int(portString) returns nil for inputs like host: and host:abc, and resolvedPort(_:) currently treats that as “no port supplied”. Those endpoints are accepted and silently connect to the default port instead of surfacing validation error.
💡 Fix sketch
- let portString = String(normalizedRemainder.dropFirst())
- guard let port = resolvedPort(Int(portString)) else { return nil }
+ let portString = String(normalizedRemainder.dropFirst())
+ guard !portString.isEmpty,
+ let port = resolvedPort(Int(portString), wasExplicit: true) else { return nil }
...
- guard let host = normalizedHost(hostPart),
- let port = resolvedPort(Int(portPart)) else {
+ guard let host = normalizedHost(hostPart),
+ !portPart.isEmpty,
+ let port = resolvedPort(Int(portPart), wasExplicit: true) else {
return nil
}
...
- private static func resolvedPort(_ candidate: Int?) -> Int? {
- guard let candidate else { return defaultPort }
+ private static func resolvedPort(_ candidate: Int?, wasExplicit: Bool = false) -> Int? {
+ guard let candidate else { return wasExplicit ? nil : defaultPort }
guard (1...65535).contains(candidate) else { return nil }
return candidate
}Also applies to: 243-245, 261-264
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/Panels/MarkdownPanel.swift` around lines 228 - 230, The code accepts
endpoints like "host:" or "host:abc" by treating a malformed explicit port as
“no port supplied” and defaulting to 5900; fix this by explicitly validating the
portString when normalizedRemainder.hasPrefix(":") — ensure portString is
non-empty and Int(portString) succeeds before calling resolvedPort, and if
parsing fails return nil (do not fall through to default port). Update the three
locations that use normalizedRemainder, portString and resolvedPort (the
occurrences around the shown snippet plus the blocks at the other reported
ranges) to first check portString.isEmpty, then let portInt = Int(portString),
then let port = resolvedPort(portInt) else return nil.
| let usernameTrimmed = username.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| let passwordTrimmed = password.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| let authComponent: String | ||
| if usernameTrimmed.isEmpty { | ||
| authComponent = "" | ||
| } else { | ||
| let encodedUser = Self.encodeUserInfo(usernameTrimmed) | ||
| if passwordTrimmed.isEmpty { | ||
| authComponent = "\(encodedUser)@" | ||
| } else { | ||
| let encodedPassword = Self.encodeUserInfo(passwordTrimmed) | ||
| authComponent = "\(encodedUser):\(encodedPassword)@" |
There was a problem hiding this comment.
Don't trim VNC credentials before using them.
Leading/trailing whitespace is significant for passwords. This mutates " secret " into "secret", so valid credentials can never authenticate. The same trimming in hasCredentialValue(...) also makes whitespace-only credentials impossible to submit.
🔐 Fix sketch
- let usernameTrimmed = username.trimmingCharacters(in: .whitespacesAndNewlines)
- let passwordTrimmed = password.trimmingCharacters(in: .whitespacesAndNewlines)
+ let usernameValue = username
+ let passwordValue = password
let authComponent: String
- if usernameTrimmed.isEmpty {
+ if usernameValue.isEmpty {
authComponent = ""
} else {
- let encodedUser = Self.encodeUserInfo(usernameTrimmed)
- if passwordTrimmed.isEmpty {
+ let encodedUser = Self.encodeUserInfo(usernameValue)
+ if passwordValue.isEmpty {
authComponent = "\(encodedUser)@"
} else {
- let encodedPassword = Self.encodeUserInfo(passwordTrimmed)
+ let encodedPassword = Self.encodeUserInfo(passwordValue)
authComponent = "\(encodedUser):\(encodedPassword)@"
}
}
...
- return !usernameInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
+ return !usernameInput.isEmpty
...
- return !passwordInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
+ return !passwordInput.isEmptyAlso applies to: 1815-1820
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/Panels/MarkdownPanel.swift` around lines 1099 - 1110, The code
currently trims username and password before encoding
(usernameTrimmed/passwordTrimmed) which strips significant leading/trailing
whitespace and breaks valid VNC credentials; remove the trimming and use the
original username and password values when building authComponent (and when
calling Self.encodeUserInfo) so whitespace is preserved, and likewise update
hasCredentialValue(...) to stop trimming input there; also apply the same change
in the other occurrence referenced (around the hasCredentialValue block at the
1815–1820 area) so whitespace-only or space-padded credentials can be submitted
and authenticated.
| func sessionSnapshot() -> SessionVncPanelSnapshot { | ||
| SessionVncPanelSnapshot( | ||
| endpointInput: endpointInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : endpointInput, | ||
| usernameInput: usernameInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : usernameInput, | ||
| autoConnect: isConnected || connectionState == .connecting | ||
| ) |
There was a problem hiding this comment.
Persist the configured auto-connect flag, not the current connection state.
This writes snapshot.autoConnect from isConnected || connectionState == .connecting, so a one-off manual connect becomes sticky across restarts, while a panel that was configured to auto-connect stops reconnecting if it happens to be disconnected/error at snapshot time. autoConnect needs to be stored on VncPanel and snapshotted directly.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/Panels/MarkdownPanel.swift` around lines 1594 - 1599, The snapshot is
using runtime connection state (isConnected || connectionState == .connecting)
instead of the configured auto-connect flag; change sessionSnapshot() to read
and persist a stored autoConnect property on VncPanel (e.g., add/use a Bool
property named autoConnect on VncPanel) and pass that value into
SessionVncPanelSnapshot(autoConnect:), rather than computing it from isConnected
or connectionState; ensure anywhere the panel configuration is created/updated
(constructors or UI bindings) sets VncPanel.autoConnect so the snapshot reflects
the user-configured setting.
| Task { [weak self] in | ||
| let resolvedHost = await VncSSHHostAliasResolver.resolve(host: target.host) ?? target.host | ||
| await MainActor.run { [weak self] in | ||
| guard let self else { return } | ||
| guard self.connectAttemptID == attemptID else { return } | ||
| guard self.activeTarget == target else { return } | ||
| self.startNativeConnection( |
There was a problem hiding this comment.
Gate native session callbacks to the current connect generation.
connectAttemptID only protects the async host-resolution task. The reused VncNativeSessionController keeps polling after disconnectFromCurrentSession(suppressUpdate: true), so stale .disconnected/.connected callbacks from the previous session can still overwrite the new panel state and even record the wrong target as “recent” before the next startNativeConnection(...) is actually running.
Also applies to: 1866-1868, 1919-1943
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/Panels/MarkdownPanel.swift` around lines 1652 - 1658, The async
host-resolution check only guards the resolution path but not later native
session callbacks, so stale VncNativeSessionController
`.connected`/`.disconnected` handlers can overwrite state; update all places
that register or handle native session callbacks (e.g., where
VncNativeSessionController invokes connection state handlers and in
startNativeConnection and any disconnectFromCurrentSession(suppressUpdate:)
callback paths) to capture the current connectAttemptID and early-return if the
stored connectAttemptID != captured attemptID, effectively gating callback work
to the same connection generation (apply the same guard to the other occurrences
noted around the 1866–1868 and 1919–1943 regions).
| if let line, let column { | ||
| components.append("line \(line):\(column)") | ||
| } else if let line { | ||
| components.append("line \(line)") |
There was a problem hiding this comment.
Localize this JS error prefix before surfacing it in the panel.
"line \(line)" is part of lastErrorDetail, so localized builds will still show English here. Please route it through String(localized:..., defaultValue:...) like the other VNC error strings.
As per coding guidelines, "All user-facing strings must be localized. Use String(localized: "key.name", defaultValue: "English text") for every string shown in the UI (labels, buttons, menus, dialogs, tooltips, error messages)."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/Panels/MarkdownPanel.swift` around lines 2054 - 2057, The JS error
"line ..." prefix is not localized; update the two components.append calls in
MarkdownPanel (the branch that appends "line \(line):\(column)" and the one that
appends "line \(line)") to use String(localized:..., defaultValue:...) like the
other VNC error strings so the UI shows localized builds correctly; pick clear
localization keys (e.g. a key for "line {line}:{column}" and one for "line
{line}") and interpolate the line/column values into the localized strings when
calling String(localized:..., defaultValue:...).
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="Sources/Panels/MarkdownPanel.swift">
<violation number="1" location="Sources/Panels/MarkdownPanel.swift:1213">
P2: `configureInteractiveControlMode()` is being called on every connected-state poll (10x/sec). Gate it to state transitions so control/input configuration is not reapplied continuously.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| setObjectBool(false, on: sessionObject, selectorName: "setHilightCursor:") | ||
| setBool(true, on: sessionObject, selectorName: "setViewerCursorVisible:") | ||
| setBool(true, on: sessionObject, selectorName: "setShouldShowCursorForUnknownCursorState:") | ||
| configureInteractiveControlMode() |
There was a problem hiding this comment.
P2: configureInteractiveControlMode() is being called on every connected-state poll (10x/sec). Gate it to state transitions so control/input configuration is not reapplied continuously.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/Panels/MarkdownPanel.swift, line 1213:
<comment>`configureInteractiveControlMode()` is being called on every connected-state poll (10x/sec). Gate it to state transitions so control/input configuration is not reapplied continuously.</comment>
<file context>
@@ -1042,7 +1210,7 @@ private final class VncNativeSessionController: NSObject {
setBool(true, on: sessionObject, selectorName: "setViewerCursorVisible:")
setBool(true, on: sessionObject, selectorName: "setShouldShowCursorForUnknownCursorState:")
- setBool(true, on: sessionObject, selectorName: "setKeyboardFocusEnabled:")
+ configureInteractiveControlMode()
setBool(true, on: sessionObject, selectorName: "setShouldSharePasteboard:")
setBool(true, on: sessionObject, selectorName: "setShouldAllowSendPasteboard:")
</file context>
| configureInteractiveControlMode() | |
| if emittedState != .connected { | |
| configureInteractiveControlMode() | |
| } |
There was a problem hiding this comment.
♻️ Duplicate comments (6)
Sources/Panels/MarkdownPanel.swift (6)
1276-1292:⚠️ Potential issue | 🟠 MajorDon't trim VNC credentials before using them.
Leading/trailing whitespace is significant for passwords. Trimming
" secret "into"secret"causes valid credentials to fail authentication.🔐 Fix sketch
let hostComponent = trimmedHost.contains(":") ? "[\(trimmedHost)]" : trimmedHost -let usernameTrimmed = username.trimmingCharacters(in: .whitespacesAndNewlines) -let passwordTrimmed = password.trimmingCharacters(in: .whitespacesAndNewlines) let authComponent: String -if usernameTrimmed.isEmpty { +if username.isEmpty { authComponent = "" } else { - let encodedUser = Self.encodeUserInfo(usernameTrimmed) - if passwordTrimmed.isEmpty { + let encodedUser = Self.encodeUserInfo(username) + if password.isEmpty { authComponent = "\(encodedUser)@" } else { - let encodedPassword = Self.encodeUserInfo(passwordTrimmed) + let encodedPassword = Self.encodeUserInfo(password) authComponent = "\(encodedUser):\(encodedPassword)@" } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Panels/MarkdownPanel.swift` around lines 1276 - 1292, The code trims VNC credentials (usernameTrimmed/passwordTrimmed) which can remove meaningful whitespace; revert to using the original username and password values when building authComponent instead of usernameTrimmed/passwordTrimmed. Replace references to usernameTrimmed and passwordTrimmed in the authComponent construction with the raw username and password variables, still passing them through Self.encodeUserInfo when creating encodedUser/encodedPassword so encoding is preserved but trimming is removed.
1772-1778:⚠️ Potential issue | 🟠 MajorPersist the configured auto-connect flag, not the current connection state.
autoConnect: isConnected || connectionState == .connectingwrites runtime state instead of user intent. A one-off manual connect becomes sticky across restarts, while a panel configured to auto-connect won't reconnect if it happens to be disconnected at snapshot time.💡 Fix sketch
Add a stored
autoConnect: Boolproperty toVncPanel, set it when the user enables auto-connect in the UI (or during init from restore), and snapshot that value:+ private var autoConnect: Bool = false + func sessionSnapshot() -> SessionVncPanelSnapshot { SessionVncPanelSnapshot( endpointInput: endpointInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : endpointInput, usernameInput: usernameInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : usernameInput, - autoConnect: isConnected || connectionState == .connecting + autoConnect: autoConnect ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Panels/MarkdownPanel.swift` around lines 1772 - 1778, The snapshot is recording runtime connection state instead of the user's intent; add a stored Bool property (e.g. autoConnect) to VncPanel, ensure the UI toggles that property when the user enables/disables auto-connect and that the init/restore path populates it, then change sessionSnapshot() to pass that stored autoConnect value into SessionVncPanelSnapshot (leave endpointInput and usernameInput handling as-is); also update any restore/serialize logic that reconstructs the panel to read/write the new autoConnect field.
2233-2237:⚠️ Potential issue | 🟡 MinorLocalize the JS error line prefix before surfacing it in the panel.
"line \(line)"is part oflastErrorDetailshown in the UI. UseString(localized:..., defaultValue:...)like other VNC error strings.💡 Fix sketch
if let line, let column { - components.append("line \(line):\(column)") + components.append( + String(localized: "vnc.error.jsLineColumn", defaultValue: "line \(line):\(column)") + ) } else if let line { - components.append("line \(line)") + components.append( + String(localized: "vnc.error.jsLine", defaultValue: "line \(line)") + ) }As per coding guidelines, "All user-facing strings must be localized. Use
String(localized: "key.name", defaultValue: "English text")for every string shown in the UI."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Panels/MarkdownPanel.swift` around lines 2233 - 2237, The UI shows JS error location strings built in MarkdownPanel.swift (the components.append calls that produce "line \(line):\(column)" and "line \(line)") and these must be localized before being added to lastErrorDetail; replace the hardcoded literals with localized strings using String(localized: "error.lineAndColumn" / "error.line", defaultValue: "line %d:%d" / "line %d") and format them with the line/column values when building components so the final lastErrorDetail remains fully localized.
1994-2001:⚠️ Potential issue | 🟡 MinorDon't trim credentials when checking for presence.
hasCredentialValuetrims whitespace, making whitespace-only credentials (e.g.," ") impossible to submit. Use.isEmptydirectly on the raw input.🔐 Fix sketch
private func hasCredentialValue(for field: VncCredentialField) -> Bool { switch field { case .username: - return !usernameInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + return !usernameInput.isEmpty case .password: - return !passwordInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + return !passwordInput.isEmpty } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Panels/MarkdownPanel.swift` around lines 1994 - 2001, The hasCredentialValue(for:) helper is incorrectly trimming inputs so whitespace-only credentials are treated as empty; update hasCredentialValue (used with VncCredentialField cases .username and .password) to check the raw usernameInput and passwordInput using .isEmpty (i.e., return !usernameInput.isEmpty / !passwordInput.isEmpty) instead of trimmingCharacters(in: .whitespacesAndNewlines) so whitespace-only values are considered present.
2098-2123:⚠️ Potential issue | 🟠 MajorGate native session callbacks to the current connect generation.
handleNativeSessionStatedoesn't checkconnectAttemptID, so callbacks from a previous session (afterdisconnect(suppressUpdate: true)) can still overwrite the new panel state. Capture the attempt ID when setting up the callback and early-return if it no longer matches.💡 Fix sketch
One approach: capture
connectAttemptIDin the callback setup and guard:+ private var activeNativeSessionAttemptID: UUID? + private func startNativeConnection(...) { guard let nativeSessionController else { ... } + let attemptID = connectAttemptID + activeNativeSessionAttemptID = attemptID nativeSessionController.connect(...) } private func handleNativeSessionState(_ state: VncPanelConnectionState, detail: String?) { + guard activeNativeSessionAttemptID == connectAttemptID else { return } switch state { ... }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Panels/MarkdownPanel.swift` around lines 2098 - 2123, handleNativeSessionState currently applies session callbacks regardless of which connect attempt they belong to, allowing stale callbacks to overwrite a newer panel state; when you register the native session callback capture the current connectAttemptID (the value updated by connect/disconnect and used by disconnect(suppressUpdate: true)) into a local constant and early-return from the callback if the panel's current connectAttemptID no longer matches that captured value, so modify the callback registration sites to check the capturedAttemptID before calling handleNativeSessionState (or have handleNativeSessionState accept the attemptID and guard) to ensure only the latest connection generation updates connectionState, lastErrorDetail and requiredCredentialFields.
228-231:⚠️ Potential issue | 🟡 MinorReject malformed explicit ports instead of defaulting them to
5900.When
normalizedRemainder.hasPrefix(":")is true but the port string is empty or non-numeric (e.g.,host:orhost:abc),Int(portString)returnsnilandresolvedPort(nil)returnsdefaultPort. This silently accepts malformed input instead of surfacing a validation error.💡 Fix sketch
guard normalizedRemainder.hasPrefix(":") else { return nil } let portString = String(normalizedRemainder.dropFirst()) -guard let port = resolvedPort(Int(portString)) else { return nil } +guard !portString.isEmpty, + let parsedPort = Int(portString), + let port = resolvedPort(parsedPort) else { return nil } return VncPanelTarget(host: host, port: port)Apply similar fix to the
colonCount == 1branch at lines 242-245.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/Panels/MarkdownPanel.swift` around lines 228 - 231, The code currently treats malformed explicit ports like "host:" or "host:abc" as valid by passing nil into resolvedPort and getting the default 5900; change the logic in the normalizedRemainder.hasPrefix(":") branch (and the colonCount == 1 branch) to explicitly validate the port string: require that portString is non-empty and Int(portString) succeeds, return nil on failure, then call resolvedPort with the parsed Int and construct VncPanelTarget(host: host, port: port); update the branches that reference normalizedRemainder, portString, resolvedPort, colonCount and VncPanelTarget accordingly.
🧹 Nitpick comments (1)
cmuxTests/TerminalAndGhosttyTests.swift (1)
4246-4302: Consider adding right/other mouse forwarding assertions in this suite.
VncNativeSessionHostViewalso routesrightMouseDownandotherMouseDown; adding direct unit assertions here would keep coverage complete for native multi-button routing.♻️ Suggested patch
private final class ProbeSessionView: NSView { var allowsFirstResponder = true var keyDownCount = 0 var mouseDownCount = 0 + var rightMouseDownCount = 0 + var otherMouseDownCount = 0 var performKeyEquivalentCount = 0 var consumeKeyEquivalent = false @@ override func mouseDown(with event: NSEvent) { _ = event mouseDownCount += 1 } + + override func rightMouseDown(with event: NSEvent) { + _ = event + rightMouseDownCount += 1 + } + + override func otherMouseDown(with event: NSEvent) { + _ = event + otherMouseDownCount += 1 + } @@ hostView.keyDown(with: makeKeyEvent(characters: "a", keyCode: 0, window: window)) hostView.mouseDown(with: makeMouseEvent(type: .leftMouseDown, location: NSPoint(x: 40, y: 40), window: window)) + hostView.rightMouseDown(with: makeMouseEvent(type: .rightMouseDown, location: NSPoint(x: 40, y: 40), window: window)) + hostView.otherMouseDown(with: makeMouseEvent(type: .otherMouseDown, location: NSPoint(x: 40, y: 40), window: window)) @@ XCTAssertEqual(sessionView.keyDownCount, 1) XCTAssertEqual(sessionView.mouseDownCount, 1) + XCTAssertEqual(sessionView.rightMouseDownCount, 1) + XCTAssertEqual(sessionView.otherMouseDownCount, 1) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cmuxTests/TerminalAndGhosttyTests.swift` around lines 4246 - 4302, Add assertions and support for right/other mouse forwarding by extending ProbeSessionView to track rightMouseDownCount and otherMouseDownCount and overriding rightMouseDown(with:) and otherMouseDown(with:) to increment them; then in testHostViewForwardsMouseAndKeyboardWhenSessionCannotBecomeFirstResponder send right and other mouse events via hostView (using makeMouseEvent with types .rightMouseDown and .otherMouseDown) and assert the corresponding sessionView.rightMouseDownCount and sessionView.otherMouseDownCount are 1, similar to the existing mouseDown/keyDown checks; update references to ProbeSessionView and testHostViewForwardsMouseAndKeyboardWhenSessionCannotBecomeFirstResponder accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@Sources/Panels/MarkdownPanel.swift`:
- Around line 1276-1292: The code trims VNC credentials
(usernameTrimmed/passwordTrimmed) which can remove meaningful whitespace; revert
to using the original username and password values when building authComponent
instead of usernameTrimmed/passwordTrimmed. Replace references to
usernameTrimmed and passwordTrimmed in the authComponent construction with the
raw username and password variables, still passing them through
Self.encodeUserInfo when creating encodedUser/encodedPassword so encoding is
preserved but trimming is removed.
- Around line 1772-1778: The snapshot is recording runtime connection state
instead of the user's intent; add a stored Bool property (e.g. autoConnect) to
VncPanel, ensure the UI toggles that property when the user enables/disables
auto-connect and that the init/restore path populates it, then change
sessionSnapshot() to pass that stored autoConnect value into
SessionVncPanelSnapshot (leave endpointInput and usernameInput handling as-is);
also update any restore/serialize logic that reconstructs the panel to
read/write the new autoConnect field.
- Around line 2233-2237: The UI shows JS error location strings built in
MarkdownPanel.swift (the components.append calls that produce "line
\(line):\(column)" and "line \(line)") and these must be localized before being
added to lastErrorDetail; replace the hardcoded literals with localized strings
using String(localized: "error.lineAndColumn" / "error.line", defaultValue:
"line %d:%d" / "line %d") and format them with the line/column values when
building components so the final lastErrorDetail remains fully localized.
- Around line 1994-2001: The hasCredentialValue(for:) helper is incorrectly
trimming inputs so whitespace-only credentials are treated as empty; update
hasCredentialValue (used with VncCredentialField cases .username and .password)
to check the raw usernameInput and passwordInput using .isEmpty (i.e., return
!usernameInput.isEmpty / !passwordInput.isEmpty) instead of
trimmingCharacters(in: .whitespacesAndNewlines) so whitespace-only values are
considered present.
- Around line 2098-2123: handleNativeSessionState currently applies session
callbacks regardless of which connect attempt they belong to, allowing stale
callbacks to overwrite a newer panel state; when you register the native session
callback capture the current connectAttemptID (the value updated by
connect/disconnect and used by disconnect(suppressUpdate: true)) into a local
constant and early-return from the callback if the panel's current
connectAttemptID no longer matches that captured value, so modify the callback
registration sites to check the capturedAttemptID before calling
handleNativeSessionState (or have handleNativeSessionState accept the attemptID
and guard) to ensure only the latest connection generation updates
connectionState, lastErrorDetail and requiredCredentialFields.
- Around line 228-231: The code currently treats malformed explicit ports like
"host:" or "host:abc" as valid by passing nil into resolvedPort and getting the
default 5900; change the logic in the normalizedRemainder.hasPrefix(":") branch
(and the colonCount == 1 branch) to explicitly validate the port string: require
that portString is non-empty and Int(portString) succeeds, return nil on
failure, then call resolvedPort with the parsed Int and construct
VncPanelTarget(host: host, port: port); update the branches that reference
normalizedRemainder, portString, resolvedPort, colonCount and VncPanelTarget
accordingly.
---
Nitpick comments:
In `@cmuxTests/TerminalAndGhosttyTests.swift`:
- Around line 4246-4302: Add assertions and support for right/other mouse
forwarding by extending ProbeSessionView to track rightMouseDownCount and
otherMouseDownCount and overriding rightMouseDown(with:) and
otherMouseDown(with:) to increment them; then in
testHostViewForwardsMouseAndKeyboardWhenSessionCannotBecomeFirstResponder send
right and other mouse events via hostView (using makeMouseEvent with types
.rightMouseDown and .otherMouseDown) and assert the corresponding
sessionView.rightMouseDownCount and sessionView.otherMouseDownCount are 1,
similar to the existing mouseDown/keyDown checks; update references to
ProbeSessionView and
testHostViewForwardsMouseAndKeyboardWhenSessionCannotBecomeFirstResponder
accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4f338c3c-a705-4b22-907c-247453d5d115
📒 Files selected for processing (2)
Sources/Panels/MarkdownPanel.swiftcmuxTests/TerminalAndGhosttyTests.swift
Summary
Verification
Summary by cubic
Adds a native VNC panel integrated with the panel system. Routes keyboard and mouse input directly into the macOS Screen Sharing session, persists session state, adds VNC input‑routing tests, bounds alias resolution, redacts text telemetry, and stabilizes CI key‑routing tests.
New Features - New features added
.vncwithVncPanelandVncPanelView: connect flow, credential prompts/validation, status, and localized strings.nativeSessionHostViewwith web view fallback; command palette entry and Empty Pane “VNC”;TabManageraccessors.CmuxSurfaceType.vncandCmuxSurfaceDefinition.vncendpoint field; session snapshots and restore.endpoint/username; query VNC viavnc_stateandsurface.vnc_state.VncPanelInputRoutingUITestscover multi‑key and mouse routing into a native session.Bug Fixes - Bug fixes implemented
Written for commit 7a123f4. Summary will update on new commits.
Summary by CodeRabbit
New Features
Tests